using System.Collections.Generic;
using System.Threading;

namespace Nice157Advices.Advices
{
    /// <summary>
    /// 建议73：避免锁定不恰当的同步对象
    /// </summary>
    public static class _73
    {
        /* NOTE:
        在选择同步对象的时候，应当始终注意以下几点：
        1）同步对象在需要同步的多个线程中是可见的同一个对象。
        2）在非静态方法中，静态变量不应作为同步对象。
        3）值类型对象不能作为同步对象。值类型在传递到另一个线程的时候会创建一个副本，这相当于每个线程锁定的也是两个对象。
        4）避免将字符串作为同步对象。字符串在CLR中会被暂存到内存里，如果有两个变量被分配了相同内容的字符串，那么这两个变量的引用会被指向同一块内存。所以，如果有两个地方同时使用了lock("abc")，那么他们实际锁定的是同一个对象，这会导致整个应用程序被阻滞。
        5）降低同步对象的可见性。一般来说，同步对象也不应该是一个公共变量或属性。
         */

        public static void Go ()
        {
            // var test = new Test ();
            // test.Run ();

            var test2_1 = new Test2 ();
            var test2_2 = new Test2 ();
            test2_1.Run1 ();
            test2_2.Run2 ();
        }

        private class Test
        {
            // 创建一个同步类型对象，默认阻塞(false)，该类型对象在发送信号完毕后(调用Set方法)，会自动把阻滞状态设置成true，ManualResetEvent就不会自动改变了
            AutoResetEvent autoSet = new AutoResetEvent (false);
            List<string> tempList = new List<string> { "init0", "init1", "init2" };

            public void Run ()
            {
                object syncObj = new object ();
                Thread t1 = new Thread (() =>
                {
                    // 等待t2开始之后通知其运行下面的代码
                    autoSet.WaitOne ();
                    lock (syncObj)
                    {
                        foreach (var item in tempList)
                        {
                            Thread.Sleep (1000);
                            System.Console.WriteLine (item);
                        }
                    }
                });
                /**
                 * 在CLR中，线程分为前台线程和后台线程，即每个线程都有一个lsBackground属性。两者在表现形式上的唯一区别是：如果前台线程不退出，应用程序的进程就会一直存在，必须所有的前台线程全部退出，应用程序才算退出。而后台进程则没有这方面的限制，如果应用程序退出，后台线程也会一并退出。
                 * 用Thread创建的线程默认是前台线程，IsBackground 默认为false。在实际工作中，应该多使用后台线程，只要关键的如正在执行事务或占有的某些非托管资源需要释放时才用使用前台线程。
                 */
                t1.IsBackground = true;
                t1.Start ();

                Thread t2 = new Thread (() =>
                {
                    // 通知t1可以执行代码
                    autoSet.Set ();
                    // 沉睡1秒为了确保删除操作在t1的迭代过程中(但t1里已经锁定了，所以会先执行t1里锁定的代码再执行此)
                    Thread.Sleep (1000);
                    lock (syncObj)
                    {
                        tempList.RemoveAt (1);
                        System.Console.WriteLine ("removed at 1");
                    }
                });
                // t2.IsBackground = true;
                t2.Start ();
            }
        }

        private class Test2
        {
            public static List<string> tempList = new List<string> { "init0", "init1", "init2" };
            static AutoResetEvent autoSet = new AutoResetEvent (false);
            // 如果这里没有使用 static 标明则会报错(collection was modified); 因为每次 new 出一个实例就会生成新的syncobj，锁定的不是同一个对象
            // 这与第二个注意事项相悖：在非静态方法中，静态变量不应该作为同步对象。这个实例代码仅出于演示的目的，在实际应用中建议不要编写此类代码。
            // NOTE:
            // 在编写多线程代码时，要遵循这样的一个原则：类型的静态方法应当保证线程安全，非静态方法不需实现线程安全。
            // 在此示例中，将syncObj 变成 static，将相当于让非静态方法具备了线程安全性，这带来的一个问题是，如果应用程序中该类型存在多个实例，在遇到这个锁的时候，它们都会产生同步，而这可能不是开发者所愿意看到的。
            static object syncObj = new object ();

            public void Run1 ()
            {
                Thread t1 = new Thread (() =>
                {
                    autoSet.WaitOne ();
                    lock (syncObj)
                    {
                        foreach (var item in tempList)
                        {
                            Thread.Sleep (1000);
                            System.Console.WriteLine (item);
                        }
                    }
                });
                // t1.IsBackground = true;
                t1.Start ();
            }

            public void Run2 ()
            {
                Thread t2 = new Thread (() =>
                {
                    autoSet.Set ();
                    Thread.Sleep (1000);
                    lock (syncObj)
                    {
                        tempList.RemoveAt (1);
                        System.Console.WriteLine ("remove at 1");
                    }
                });
                // t2.IsBackground = true;
                t2.Start ();
            }
        }
    }
}